/******************************************************************************* * Copyright (c) 2005, 2015 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.internal.contexts; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import org.eclipse.core.commands.contexts.ContextManager; import org.eclipse.core.commands.util.Tracing; import org.eclipse.core.expressions.Expression; import org.eclipse.core.runtime.Assert; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.ActiveShellExpression; import org.eclipse.ui.ISources; import org.eclipse.ui.contexts.IContextActivation; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.internal.misc.Policy; import org.eclipse.ui.internal.services.ExpressionAuthority; /** * <p> * A central authority for deciding activation of contexts. This authority * listens to a variety of incoming sources, and updates the underlying context * manager if changes occur. * </p> * * @since 3.1 */ public final class ContextAuthority extends ExpressionAuthority { /** * The default size of the set containing the activations to recompute. This * is more than enough to cover the average case. */ private static final int ACTIVATIONS_TO_RECOMPUTE_SIZE = 4; /** * Whether the context authority should kick into debugging mode. This * causes the unresolvable handler conflicts to be printed to the console. */ private static final boolean DEBUG = Policy.DEBUG_CONTEXTS; /** * Whether the performance information should be printed about the * performance of the context authority. */ private static final boolean DEBUG_PERFORMANCE = Policy.DEBUG_CONTEXTS_PERFORMANCE; /** * The name of the data tag containing the dispose listener information. */ private static final String DISPOSE_LISTENER = "org.eclipse.ui.internal.contexts.ContextAuthority"; //$NON-NLS-1$ /** * The component name to print when displaying tracing information. */ private static final String TRACING_COMPONENT = "CONTEXTS"; //$NON-NLS-1$ /** * A bucket sort of the context activations based on source priority. Each * activation will appear only once per set, but may appear in multiple * sets. If no activations are defined for a particular priority level, then * the array at that index will only contain <code>null</code>. */ private final Set[] activationsBySourcePriority = new Set[33]; /** * This is a map of context activations (<code>Collection</code> of * <code>IContextActivation</code>) sorted by context identifier (<code>String</code>). * If there is only one context activation for a context, then the * <code>Collection</code> is replaced by a * <code>IContextActivation</code>. If there is no activation, the entry * should be removed entirely. */ private final Map contextActivationsByContextId = new HashMap(); /** * The context manager that should be updated when the contexts are * changing. */ private final ContextManager contextManager; /** * The context service that should be used for authority-managed * shell-related contexts. This value is never <code>null</code>. */ private final IContextService contextService; /** * This is a map of shell to a list of activations. When a shell is * registered, it is added to this map with the list of activation that * should be submitted when the shell is active. When the shell is * deactivated, this same list should be withdrawn. A shell is removed from * this map using the {@link #unregisterShell(Shell)}method. This value may * be empty, but is never <code>null</code>. The <code>null</code> key * is reserved for active shells that have not been registered but have a * parent (i.e., default dialog service). */ private final Map registeredWindows = new WeakHashMap(); /** * Constructs a new instance of <code>ContextAuthority</code>. * * @param contextManager * The context manager from which contexts can be retrieved (to * update their active state); must not be <code>null</code>. * @param contextService * The workbench context service for which this authority is * acting. This allows the authority to manage shell-specific * contexts. This value must not be <code>null</code>. */ ContextAuthority(final ContextManager contextManager, final IContextService contextService) { if (contextManager == null) { throw new NullPointerException( "The context authority needs a context manager"); //$NON-NLS-1$ } if (contextService == null) { throw new NullPointerException( "The context authority needs an evaluation context"); //$NON-NLS-1$ } this.contextManager = contextManager; this.contextService = contextService; } /** * Activates a context on the workbench. This will add it to a master list. * * @param activation * The activation; must not be <code>null</code>. */ final void activateContext(final IContextActivation activation) { // First we update the contextActivationsByContextId map. final String contextId = activation.getContextId(); final Object value = contextActivationsByContextId.get(contextId); if (value instanceof Collection) { final Collection contextActivations = (Collection) value; if (!contextActivations.contains(activation)) { contextActivations.add(activation); updateContext(contextId, containsActive(contextActivations)); } } else if (value instanceof IContextActivation) { if (value != activation) { final Collection contextActivations = new ArrayList(2); contextActivations.add(value); contextActivations.add(activation); contextActivationsByContextId .put(contextId, contextActivations); updateContext(contextId, containsActive(contextActivations)); } } else { contextActivationsByContextId.put(contextId, activation); updateContext(contextId, evaluate(activation)); } // Next we update the source priority bucket sort of activations. final int sourcePriority = activation.getSourcePriority(); for (int i = 1; i <= 32; i++) { if ((sourcePriority & (1 << i)) != 0) { Set activations = activationsBySourcePriority[i]; if (activations == null) { activations = new HashSet(1); activationsBySourcePriority[i] = activations; } activations.add(activation); } } } /** * Checks whether the new active shell is registered. If it is already * registered, then it does no work. If it is not registered, then it checks * what type of contexts the shell should have by default. This is * determined by parenting. A shell with no parent receives no contexts. A * shell with a parent, receives the dialog contexts. * * @param newShell * The newly active shell; may be <code>null</code> or * disposed. * @param oldShell * The previously active shell; may be <code>null</code> or * disposed. */ private final void checkWindowType(final Shell newShell, final Shell oldShell) { /* * If the previous active shell was recognized as a dialog by default, * then remove its submissions. */ Collection oldActivations = (Collection) registeredWindows .get(oldShell); if (oldActivations == null) { /* * The old shell wasn't registered. So, we need to check if it was * considered a dialog by default. */ oldActivations = (Collection) registeredWindows.get(null); if (oldActivations != null) { final Iterator oldActivationItr = oldActivations.iterator(); while (oldActivationItr.hasNext()) { final IContextActivation activation = (IContextActivation) oldActivationItr .next(); deactivateContext(activation); } } } /* * If the new active shell is recognized as a dialog by default, then * create some submissions, remember them, and submit them for * processing. */ if ((newShell != null) && (!newShell.isDisposed())) { final Collection newActivations; if ((newShell.getParent() != null) && (registeredWindows.get(newShell) == null)) { // This is a dialog by default. newActivations = new ArrayList(); final Expression expression = new ActiveShellExpression( newShell); final IContextActivation dialogWindowActivation = new ContextActivation( IContextService.CONTEXT_ID_DIALOG_AND_WINDOW, expression, contextService); activateContext(dialogWindowActivation); newActivations.add(dialogWindowActivation); final IContextActivation dialogActivation = new ContextActivation( IContextService.CONTEXT_ID_DIALOG, expression, contextService); activateContext(dialogActivation); newActivations.add(dialogActivation); registeredWindows.put(null, newActivations); /* * Make sure the submissions will be removed in event of * disposal. This is really just a paranoid check. The * "oldSubmissions" code above should take care of this. */ newShell.addDisposeListener(new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { registeredWindows.remove(null); if (!newShell.isDisposed()) { newShell.removeDisposeListener(this); } /* * In the case where a dispose has happened, we are * expecting an activation event to arrive at some point * in the future. If we process the submissions now, * then we will update the activeShell before * checkWindowType is called. This means that dialogs * won't be recognized as dialogs. */ final Iterator newActivationItr = newActivations .iterator(); while (newActivationItr.hasNext()) { deactivateContext((IContextActivation) newActivationItr .next()); } } }); } else { // Shells that are not dialogs by default must register. newActivations = null; } } } /** * Returns a subset of the given <code>activations</code> containing only * those that are active * * @param activations * The activations to trim; must not be <code>null</code>, but * may be empty. * @return <code>true</code> if there is at least one active context; * <code>false</code> otherwise. */ private final boolean containsActive(final Collection activations) { final Iterator activationItr = activations.iterator(); while (activationItr.hasNext()) { final IContextActivation activation = (IContextActivation) activationItr .next(); if (evaluate(activation)) { return true; } } return false; } /** * Removes an activation for a context on the workbench. This will remove it * from the master list, and update the appropriate context, if necessary. * * @param activation * The activation; must not be <code>null</code>. */ final void deactivateContext(final IContextActivation activation) { // First we update the handlerActivationsByCommandId map. final String contextId = activation.getContextId(); final Object value = contextActivationsByContextId.get(contextId); if (value instanceof Collection) { final Collection contextActivations = (Collection) value; if (contextActivations.contains(activation)) { contextActivations.remove(activation); if (contextActivations.isEmpty()) { contextActivationsByContextId.remove(contextId); updateContext(contextId, false); } else if (contextActivations.size() == 1) { final IContextActivation remainingActivation = (IContextActivation) contextActivations .iterator().next(); contextActivationsByContextId.put(contextId, remainingActivation); updateContext(contextId, evaluate(remainingActivation)); } else { updateContext(contextId, containsActive(contextActivations)); } } } else if (value instanceof IContextActivation) { if (value == activation) { contextActivationsByContextId.remove(contextId); updateContext(contextId, false); } } // Next we update the source priority bucket sort of activations. final int sourcePriority = activation.getSourcePriority(); for (int i = 1; i <= 32; i++) { if ((sourcePriority & (1 << i)) != 0) { final Set activations = activationsBySourcePriority[i]; if (activations == null) { continue; } activations.remove(activation); if (activations.isEmpty()) { activationsBySourcePriority[i] = null; } } } } /** * Returns the currently active shell. * * @return The currently active shell; may be <code>null</code>. */ final Shell getActiveShell() { return (Shell) getVariable(ISources.ACTIVE_SHELL_NAME); } /** * Returns the shell type for the given shell. * * @param shell * The shell for which the type should be determined. If this * value is <code>null</code>, then * <code>IWorkbenchContextSupport.TYPE_NONE</code> is returned. * @return <code>IWorkbenchContextSupport.TYPE_WINDOW</code>, * <code>IWorkbenchContextSupport.TYPE_DIALOG</code>, or * <code>IWorkbenchContextSupport.TYPE_NONE</code>. */ public final int getShellType(final Shell shell) { // If the shell is null, then return none. if (shell == null) { return IContextService.TYPE_NONE; } final Collection activations = (Collection) registeredWindows .get(shell); if (activations != null) { // The shell is registered, so check what type it was registered as. if (activations.isEmpty()) { // It was registered as none. return IContextService.TYPE_NONE; } // Look for the right type of context id. final Iterator activationItr = activations.iterator(); while (activationItr.hasNext()) { final IContextActivation activation = (IContextActivation) activationItr .next(); final String contextId = activation.getContextId(); if (contextId == IContextService.CONTEXT_ID_DIALOG) { return IContextService.TYPE_DIALOG; } else if (contextId == IContextService.CONTEXT_ID_WINDOW) { return IContextService.TYPE_WINDOW; } } // This shouldn't be possible. Assert .isTrue( false, "A registered shell should have at least one submission matching TYPE_WINDOW or TYPE_DIALOG"); //$NON-NLS-1$ return IContextService.TYPE_NONE; // not reachable } else if (shell.getParent() != null) { /* * The shell is not registered, but it has a parent. It is therefore * considered a dialog by default. */ return IContextService.TYPE_DIALOG; } else { /* * The shell is not registered, but has no parent. It gets no key * bindings. */ return IContextService.TYPE_NONE; } } /** * <p> * Registers a shell to automatically promote or demote some basic types of * contexts. The "In Dialogs" and "In Windows" contexts are provided by the * system. This a convenience method to ensure that these contexts are * promoted when the given is shell is active. * </p> * <p> * If a shell is registered as a window, then the "In Windows" context is * enabled when that shell is active. If a shell is registered as a dialog -- * or is not registered, but has a parent shell -- then the "In Dialogs" * context is enabled when that shell is active. If the shell is registered * as none -- or is not registered, but has no parent shell -- then the * neither of the contexts will be enabled (by us -- someone else can always * enabled them). * </p> * <p> * If the provided shell has already been registered, then this method will * change the registration. * </p> * * @param shell * The shell to register for key bindings; must not be * <code>null</code>. * @param type * The type of shell being registered. This value must be one of * the constants given in this interface. * * @return <code>true</code> if the shell had already been registered * (i.e., the registration has changed); <code>false</code> * otherwise. */ public final boolean registerShell(final Shell shell, final int type) { // We do not allow null shell registration. It is reserved. if (shell == null) { throw new NullPointerException("The shell was null"); //$NON-NLS-1$ } // Debugging output if (DEBUG) { final StringBuffer buffer = new StringBuffer("register shell '"); //$NON-NLS-1$ buffer.append(shell); buffer.append("' as "); //$NON-NLS-1$ switch (type) { case IContextService.TYPE_DIALOG: buffer.append("dialog"); //$NON-NLS-1$ break; case IContextService.TYPE_WINDOW: buffer.append("window"); //$NON-NLS-1$ break; case IContextService.TYPE_NONE: buffer.append("none"); //$NON-NLS-1$ break; default: buffer.append("unknown"); //$NON-NLS-1$ break; } Tracing.printTrace(TRACING_COMPONENT, buffer.toString()); } // Build the list of submissions. final List activations = new ArrayList(); Expression expression; IContextActivation dialogWindowActivation; switch (type) { case IContextService.TYPE_DIALOG: expression = new ActiveShellExpression(shell); dialogWindowActivation = new ContextActivation( IContextService.CONTEXT_ID_DIALOG_AND_WINDOW, expression, contextService); activateContext(dialogWindowActivation); activations.add(dialogWindowActivation); final IContextActivation dialogActivation = new ContextActivation( IContextService.CONTEXT_ID_DIALOG, expression, contextService); activateContext(dialogActivation); activations.add(dialogActivation); break; case IContextService.TYPE_NONE: break; case IContextService.TYPE_WINDOW: expression = new ActiveShellExpression(shell); dialogWindowActivation = new ContextActivation( IContextService.CONTEXT_ID_DIALOG_AND_WINDOW, expression, contextService); activateContext(dialogWindowActivation); activations.add(dialogWindowActivation); final IContextActivation windowActivation = new ContextActivation( IContextService.CONTEXT_ID_WINDOW, expression, contextService); activateContext(windowActivation); activations.add(windowActivation); break; default: throw new IllegalArgumentException("The type is not recognized: " //$NON-NLS-1$ + type); } // Check to see if the activations are already present. boolean returnValue = false; final Collection previousActivations = (Collection) registeredWindows .get(shell); if (previousActivations != null) { returnValue = true; final Iterator previousActivationItr = previousActivations .iterator(); while (previousActivationItr.hasNext()) { final IContextActivation activation = (IContextActivation) previousActivationItr .next(); deactivateContext(activation); } } // Add the new submissions, and force some reprocessing to occur. registeredWindows.put(shell, activations); /* * Remember the dispose listener so that we can remove it later if we * unregister the shell. */ final DisposeListener shellDisposeListener = new DisposeListener() { @Override public void widgetDisposed(DisposeEvent e) { registeredWindows.remove(shell); if (!shell.isDisposed()) { shell.removeDisposeListener(this); } /* * In the case where a dispose has happened, we are expecting an * activation event to arrive at some point in the future. If we * process the submissions now, then we will update the * activeShell before checkWindowType is called. This means that * dialogs won't be recognized as dialogs. */ final Iterator activationItr = activations.iterator(); while (activationItr.hasNext()) { deactivateContext((IContextActivation) activationItr.next()); } } }; // Make sure the submissions will be removed in event of disposal. shell.addDisposeListener(shellDisposeListener); shell.setData(DISPOSE_LISTENER, shellDisposeListener); return returnValue; } /** * Carries out the actual source change notification. It assumed that by the * time this method is called, <code>context</code> is up-to-date with the * current state of the application. * * @param sourcePriority * A bit mask of all the source priorities that have changed. */ @Override protected final void sourceChanged(final int sourcePriority) { // If tracing, then track how long it takes to process the activations. long startTime = 0L; if (DEBUG_PERFORMANCE) { startTime = System.currentTimeMillis(); } /* * In this first phase, we cycle through all of the activations that * could have potentially changed. Each such activation is added to a * set for future processing. We add it to a set so that we avoid * handling any individual activation more than once. */ final Set activationsToRecompute = new HashSet( ACTIVATIONS_TO_RECOMPUTE_SIZE); for (int i = 1; i <= 32; i++) { if ((sourcePriority & (1 << i)) != 0) { final Collection activations = activationsBySourcePriority[i]; if (activations != null) { final Iterator activationItr = activations.iterator(); while (activationItr.hasNext()) { activationsToRecompute.add(activationItr.next()); } } } } /* * For every activation, we recompute its active state, and check * whether it has changed. If it has changed, then we take note of the * context identifier so we can update the context later. */ final Collection changedContextIds = new ArrayList( activationsToRecompute.size()); final Iterator activationItr = activationsToRecompute.iterator(); while (activationItr.hasNext()) { final IContextActivation activation = (IContextActivation) activationItr .next(); final boolean currentActive = evaluate(activation); activation.clearResult(); final boolean newActive = evaluate(activation); if (newActive != currentActive) { changedContextIds.add(activation.getContextId()); } } try { contextManager.deferUpdates(true); /* * For every context identifier with a changed activation, we * resolve conflicts and trigger an update. */ final Iterator changedContextIdItr = changedContextIds.iterator(); while (changedContextIdItr.hasNext()) { final String contextId = (String) changedContextIdItr.next(); final Object value = contextActivationsByContextId .get(contextId); if (value instanceof IContextActivation) { final IContextActivation activation = (IContextActivation) value; updateContext(contextId, evaluate(activation)); } else if (value instanceof Collection) { updateContext(contextId, containsActive((Collection) value)); } else { updateContext(contextId, false); } } } finally { contextManager.deferUpdates(false); } // If tracing performance, then print the results. if (DEBUG_PERFORMANCE) { final long elapsedTime = System.currentTimeMillis() - startTime; final int size = activationsToRecompute.size(); if (size > 0) { Tracing.printTrace(TRACING_COMPONENT, size + " activations recomputed in " + elapsedTime + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ } } } /** * <p> * Unregisters a shell that was previously registered. After this method * completes, the shell will be treated as if it had never been registered * at all. If you have registered a shell, you should ensure that this * method is called when the shell is disposed. Otherwise, a potential * memory leak will exist. * </p> * <p> * If the shell was never registered, or if the shell is <code>null</code>, * then this method returns <code>false</code> and does nothing. * * @param shell * The shell to be unregistered; does nothing if this value is * <code>null</code>. * * @return <code>true</code> if the shell had been registered; * <code>false</code> otherwise. */ public final boolean unregisterShell(final Shell shell) { // Don't allow this method to play with the special null slot. if (shell == null) { return false; } /* * If we're unregistering the shell but we're not about to dispose it, * then we'll end up leaking the DisposeListener unless we remove it * here. */ if (!shell.isDisposed()) { final DisposeListener oldListener = (DisposeListener) shell .getData(DISPOSE_LISTENER); if (oldListener != null) { shell.removeDisposeListener(oldListener); } } Collection previousActivations = (Collection) registeredWindows .get(shell); if (previousActivations != null) { registeredWindows.remove(shell); final Iterator previousActivationItr = previousActivations .iterator(); while (previousActivationItr.hasNext()) { final IContextActivation activation = (IContextActivation) previousActivationItr .next(); deactivateContext(activation); } return true; } return false; } /** * Updates the context with the given context activation. * * @param contextId * The identifier of the context which should be updated; must * not be <code>null</code>. * @param active * Whether the context should be active; <code>false</code> * otherwise. */ private final void updateContext(final String contextId, final boolean active) { if (active) { contextManager.addActiveContext(contextId); } else { contextManager.removeActiveContext(contextId); } } /** * Updates this authority's evaluation context. If the changed variable is * the <code>ISources.ACTIVE_SHELL_NAME</code> variable, then this also * triggers an update of the shell-specific contexts. For example, if a * dialog becomes active, then the dialog context will be activated by this * method. * * @param name * The name of the variable to update; must not be * <code>null</code>. * @param value * The new value of the variable. If this value is * <code>null</code>, then the variable is removed. */ @Override protected final void updateEvaluationContext(final String name, final Object value) { /* * Bug 84056. If we update the active workbench window, then we risk * falling back to that shell when the active shell has registered as * "none". */ if ((name != null) && (!ISources.ACTIVE_WORKBENCH_WINDOW_SHELL_NAME.equals(name))) { /* * We need to track shell activation ourselves, as some special * contexts are automatically activated in response to different * types of shells becoming active. */ if (ISources.ACTIVE_SHELL_NAME.equals(name)) { checkWindowType((Shell) value, (Shell) getVariable(ISources.ACTIVE_SHELL_NAME)); } // Update the evaluation context itself. changeVariable(name, value); } } /** * <p> * Bug 95792. A mechanism by which the key binding architecture can force an * update of the contexts (based on the active shell) before trying to * execute a command. This mechanism is required for GTK+ only. * </p> * <p> * DO NOT CALL THIS METHOD. * </p> */ final void updateShellKludge() { updateCurrentState(); sourceChanged(ISources.ACTIVE_SHELL); } }